如何将两个 GitHub 仓库合并而不丢失历史记录?
这是一篇 Mozilla 的技术博客文章,讲述了他们在 MDN Web Docs 项目中,如何把较小的示例代码仓库合并到较大的父仓库中所遇到的问题以及解决方案。
我们正在 MDN Web Docs 项目中,把较小的示例代码仓库合并到较大的父仓库中。尽管我们最初以为简单地把文件从一个仓库复制到新的仓库会导致提交历史丢失,但我们觉得这个方法也许还可以。毕竟,我们并没有删除旧仓库,只是将其存档。
不过,在完成几次合并后,我们收到了一位社区成员的反馈,指出在移动这些仓库时丢失历史记录并不理想,而且其实有一种比较简单的方法可以避免这种情况。我尝试了几种方法,最终采用了 Eric Lee 在其博客上分享的一种策略。
这个方法使用基本的 git 命令,将旧仓库的所有历史记录迁移到新仓库中,而不需要借助任何特殊工具。
实验开始
在这个实验中,我用到了 sw-test
仓库,目的是将其合并到 dom-examples
仓库。
以下是 Eric 描述的第一步:
# Assume the current directory is where we want the new repository to be created
# Create the new repository
git init
# Before we do a merge, we need to have an initial commit, so we’ll make a dummy commit
dir > deleteme.txt
git add .
git commit -m “Initial dummy commit”
# Add a remote for and fetch the old repo
git remote add -f old_a <OldA repo URL>
# Merge the files from old_a/master into new/master
git merge old_a/master
因为我的目标仓库已有一些历史记录,所以我跳过了一些步骤,直接从 git remote ...
开始,如下所示:
git clone https://github.com/mdn/dom-examples.git
cd dom-examples
在这个仓库中运行 git log
后,我看到以下的提交记录:
commit cdfd2aeb93cb4bd8456345881997fcec1057efbb (HEAD -> master, upstream/master)
Merge: 1c7ff6e dfe991b
Author:
Date: Fri Aug 5 10:21:27 2022 +0200
Merge pull request #143 from mdn/sideshowbarker/webgl-sample6-UNPACK_FLIP_Y_WEBGL
“Using textures in WebGL”: Fix orientation of Firefox logo
commit dfe991b5d1b34a492ccd524131982e140cf1e555
Author:
Date: Fri Aug 5 17:08:50 2022 +0900
“Using textures in WebGL”: Fix orientation of Firefox logo
Fixes <https://github.com/mdn/content/issues/10132>
commit 1c7ff6eec8bb0fff5630a66a32d1b9b6b9d5a6e5
Merge: be41273 5618100
Author:
Date: Fri Aug 5 09:01:56 2022 +0200
Merge pull request #142 from mdn/sideshowbarker/webgl-demo-add-playsInline-drop-autoplay
WebGL sample8: Drop “autoplay”; add “playsInline”
commit 56181007b7a33907097d767dfe837bb5573dcd38
Author:
Date: Fri Aug 5 13:41:45 2022 +0900
根据当前的设置,我可以继续执行 git remote
命令,但我担心当前目录中是否有与 service worker 仓库冲突的文件或文件夹。我查了一下,看是否有人遇到过类似的情况,但没有找到合适的答案。突然,我想到了一个解决办法!我需要准备好 service worker 仓库,使其能够被安全地移动。
具体来说,我需要在 sw-test
仓库的根目录下创建一个名为 service-worker/sw-test
的新目录,并将所有相关文件移动到这个新子目录中。这样一来,我就可以安全地将其合并进 dom-examples
,因为所有文件都已经包含在一个子文件夹中。
首先,需要克隆我们想要合并进 dom-examples
的仓库。
git clone https://github.com/mdn/sw-test.git
cd sw-test
好的,现在我们可以开始准备这个项目了。第一步是创建一个新的子目录。
mkdir service-worker
mkdir service-worker/sw-test
准备好这些后,我们只需要将根目录中的所有内容移动到子目录中。为此,我们将使用mv
命令:
注意: 现在这个阶段请不要运行以下命令。
# enable extendedglob for ZSH
set -o extendedglob
mv ^sw-test(D) service-worker/swtest
上面的命令比你想象的要复杂一些,因为它使用了否定语法。下一节会解释使用这种语法的原因以及如何启用它。
如何在使用 mv
命令时排除子目录
虽然目标看起来很简单,但我花了很长时间才弄清楚如何让最后一个移动命令生效,期间令人烦恼。我查阅了许多 StackOverflow 的帖子、博客文章和命令手册页,但结果不尽如人意。最终,我在两个 StackOverflow 帖子中找到了答案。
- How to move all files in a folder to a sub folder in zsh w/ Mac OS X?
- How to move all files in current folder to subfolder?
为了节省你的时间,我先说明一下我是怎么做的。
首先需要注意的是,我在 Mac 上使用 ZSH(自 macOS Catalina 起这是默认的 shell)。根据你使用的 shell,下面的步骤可能会有所不同。
在新版 ZSH 中,你可以用 set -o
和 set +o
命令来启用或禁用设置。为了启用 extendedglob
,我使用了以下命令:
# Yes, this _enables_ it
set -o extendedglob
在旧版本的 ZSH 中,可以使用 setopt
和 unsetopt
命令。
setopt extendedglob
使用 bash
,你可以使用以下命令达到同样的效果:
shopt -s extglob
你可能会问,为什么必须这么做?如果不这样,你将无法使用我在上述移动命令中使用的否定运算符,而这正是整个操作的关键。例如,如果你进行以下操作:
mkdir service-worker
mv * service-worker/sw-test
虽然它会运行,但你会看到一个像这样的错误消息:
mv: rename service-worker to service-worker/sw-test/service-worker: Invalid argument
我们想告诉操作系统,将所有内容移到新子文件夹中,但不包括这个子文件夹本身。因此,我们需要使用否定语法。不过,默认情况下它是关闭的,因为如果文件名中包含 extendedglob
模式(例如 ^
),可能会引发问题。我们需要手动启用它。
注意:完成移动操作后,可能还需要将其禁用。
现在我们了解了如何以及为什么要启用 extendedglob
,接下来就可以开始使用这个功能了。
注意:此阶段请不要运行以下任何命令。
mv ^sw-test(D) service-worker/sw-test
这意味着:
- 将当前目录中的所有文件移动到
service-worker/sw-test
中。 - 不要移动
service-worker
目录本身。 - (D) 选项告诉移动命令也移动所有隐藏文件,例如
.gitignore
,以及隐藏文件夹,例如.git
。
注意:我发现当我输入
mv ^sw-test
并按下 Tab 键时,终端会将命令自动扩展为mv CODE_OF_CONDUCT.md LICENSE README.md app.js gallery image-list.js index.html service-worker star-wars-logo.jpg style.css sw.js
。而如果我输入mv ^sw-test(D)
并按下 Tab 键,它会扩展为mv .git .prettierrc CODE_OF_CONDUCT.md LICENSE README.md app.js gallery image-list.js index.html service-worker star-wars-logo.jpg style.css sw.js
。这非常有趣,因为它清晰地展示了在后台发生了什么。这样你就能清楚看到使用(D)
的效果。我不确定这是否只是 ZSH 的原生功能,还是由于我使用的终端插件之一,例如 Fig 实现的。实际效果可能会有所不同。
处理隐藏文件和创建拉取请求
尽管一次性移动所有隐藏文件和文件夹听起来很方便,但实际上会引发一些问题。由于 .git
文件夹被移动到了新的子文件夹中,导致我们的根目录不再被识别为 Git 仓库,这是一个问题。
因此,我不会使用 (D)
运行上述命令,而是将隐藏文件作为一个单独步骤来移动。相应地,我将运行以下命令:
mv ^(sw-test|service-worker) service-worker/sw-test
此时,如果你运行 ls
命令,你会看到似乎所有文件都被移动了。但实际情况并非如此,因为 ls
命令不会列出隐藏文件。要查看隐藏文件,你需要传递 -A
参数,如下所示:
ls -A
现在你将会看到以下内容:
❯ ls -A
.git .prettierrc service-worker
看了上面的输出后,我意识到根本不需要移动 .git
文件夹。我现在只需要运行以下命令:
mv .prettierrc service-worker
运行上述命令后,ls -A
将会输出以下内容:
❯ ls -A
.git simple-service-worker
是时候庆祝一下了 😁
既然我们已经成功地将所有内容移到了新的子目录中,现在可以继续了。然而,我发现自己忘了为这项工作创建一个功能分支。
不过没关系。我只需要运行命令 git switch -C prepare-repo-for-move
。此时运行 git status
应该会输出类似这样的信息:
❯ git status
On branch prepare-repo-for-move
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: .prettierrc
deleted: CODE_OF_CONDUCT.md
deleted: LICENSE
deleted: README.md
deleted: app.js
deleted: gallery/bountyHunters.jpg
deleted: gallery/myLittleVader.jpg
deleted: gallery/snowTroopers.jpg
deleted: image-list.js
deleted: index.html
deleted: star-wars-logo.jpg
deleted: style.css
deleted: sw.js
Untracked files:
(use "git add <file>..." to include in what will be committed)
service-worker/
no changes added to commit (use "git add" and/or "git commit -a")
很好!让我们添加这些更改并提交。
git add .
git commit -m 'Moved all source files into new subdirectory'
现在我们要提交我们的修改并打开一个合并请求。
太棒了!让我们提交吧:
git push origin prepare-repo-for-move
打开你在 GitHub 上的代码库。你会看到横幅上显示“mv-files-into-subdir had recent pushes less than a minute ago”并带有一个 Compare & pull request 按钮。
点击按钮并按照提示步骤新建 pull request. 当 pull request 显示为绿色且准备好合并时,就可以合并了!
注意:根据你的工作流程,此时你可以请求团队成员审核你所提议的更改,然后再进行合并。另外,查看“Files changed”选项卡也是一个好习惯,以确保没有任何非预期的改动包含在此次拉取请求中。如果有任何冲突阻止你的拉取请求被合并,GitHub 会发出警告,你需要解决这些冲突。这些问题可以直接在 GitHub.com 上解决,也可以在本地解决后再推送到 GitHub 作为一个单独的提交。
当你回到 GitHub 的代码视图时,你会看到新的子目录和 .gitignore
文件。
现在,我们的代码库已经准备好合并了。
合并我们的代码库
在终端中,你需要切换回 main
分支:
git switch main
你现在可以安全地删除特性分支,并从远程仓库拉取最新更改。
git branch -D prepare-repo-for-move
git pull origin main
在拉取最新版本后运行 ls -A
现在应显示以下内容:
❯ ls -A
.git README.md service-worker
此外,在根目录下执行 git log
命令后,会看到下面的内容:
commit 8fdfe7379130b8d6ea13ea8bf14a0bb45ad725d0 (HEAD -> gh-pages, origin/gh-pages, origin/HEAD)
Author: Schalk Neethling
Date: Thu Aug 11 22:56:48 2022 +0200
Create README.md
commit 254a95749c4cc3d7d2c7ec8a5902bea225870176
Merge: f5c319b bc2cdd9
Author: Schalk Neethling
Date: Thu Aug 11 22:55:26 2022 +0200
Merge pull request #45 from mdn/prepare-repo-for-move
chore: prepare repo for move to dom-examples
commit bc2cdd939f568380ce03d56f50f16f2dc98d750c (origin/prepare-repo-for-move)
Author: Schalk Neethling
Date: Thu Aug 11 22:53:13 2022 +0200
chore: prepare repo for move to dom-examples
Prepping the repository for the move to dom-examples
commit f5c319be3b8d4f14a1505173910877ca3bb429e5
Merge: d587747 2ed0eff
Author: Ruth John
Date: Fri Mar 18 12:24:09 2022 +0000
Merge pull request #43 from SimonSiefke/add-navigation-preload
以下是我们之前中断时剩下的操作指令。
# Add a remote for and fetch the old repo
git remote add -f old_a <OldA repo URL>
# Merge the files from old_a/master into new/master
git merge old_a/master
好了,我们总结一下步骤。首先,我们需要进入目标项目的根目录。在这个例子中,为 dom-examples
目录。进入根目录后,运行以下命令:
git remote add -f swtest https://github.com/mdn/sw-test.git
注意:
-f
命令 Git 拉取远程分支。ssw
是你给远程仓库起的名字,因此它可以是任意名称。
运行该命令后,我得到了以下输出:
❯ git remote add -f swtest https://github.com/mdn/sw-test.git
Updating swtest
remote: Enumerating objects: 500, done.
remote: Counting objects: 100% (75/75), done.
remote: Compressing objects: 100% (57/57), done.
remote: Total 500 (delta 35), reused 45 (delta 15), pack-reused 425
Receiving objects: 100% (500/500), 759.76 KiB | 981.00 KiB/s, done.
Resolving deltas: 100% (269/269), done.
From <https://github.com/mdn/sw-test>
* [new branch] gh-pages -> swtest/gh-pages
* [new branch] master -> swtest/master
* [new branch] move-prettierrc -> swtest/move-prettierrc
* [new branch] rename-sw-test -> swtest/rename-sw-test
提示:虽然我们在本地已经删除了分支,但这不会自动同步到远程仓库,所以你仍然会看到
rename-sw-test
分支的引用。如果你想在远程仓库删除它,需要在该仓库的根目录下运行以下命令:git push origin :rename-sw-test
(如果你已经配置了仓库“自动删除头分支”,这将为你自动删除)
只剩下几个命令了。
提示:此时请不要运行以下命令。
git merge swtest/gh-pages
在传统的图像压缩算法中,例如 JPEG 和 PNG,通过压缩和解压图像数据来减少文件大小。然而,这些方法依赖于特定的编码方式,可能无法充分利用图像中的冗余数据。近来,基于 Transformer 的方法取得了显著进展,表明在图像压缩任务上具有更大的潜力 [20]。这些方法利用大语言模型 (LLM) 的能力,更好地捕捉图像数据中的复杂模式和关系。
图 1: 基于 Transformer 的图像压缩模型框架示意图
表 1: 传统图像压缩算法与基于 Transformer 的方法在不同压缩率下的对比结果
不同于 JPEG 和 PNG 等传统压缩方法,基于 Transformer 的图像压缩方法不依赖固定的编码方案。相反,它们使用一种称为 Token 的机制将图像分割成一系列 Tokens,并使用 Transformer 模型预测和压缩这些 Tokens。这样的方法不仅能显著提高压缩率,还能在解压缩时保留更多的图像细节和质量 [20]。
❯ git merge swtest/gh-pages
fatal: refusing to merge unrelated histories
这正是我想要的效果,对吧?默认情况下,merge
命令就是这样工作的,但你也可以通过传递一个参数来启用该行为。
git merge swtest/gh-pages --allow-unrelated-histories
注意:为什么要用
gh-pages
分支?通常我们合并的分支是main
,但对于这个特定的仓库,默认分支是gh-pages
。早期在使用 GitHub Pages 时,需要有一个名为gh-pages
的分支,GitHub 会自动将这个分支部署到类似 mdn.github.io/sw-test 的 URL。
运行上述命令后,我得到了以下结果:
❯ git merge swtest/gh-pages --allow-unrelated-histories
Auto-merging README.md
CONFLICT (add/add): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
哦,对了。由于我们当前的项目和正在合并的项目中都有一个 README.md
文件,所以 Git 在询问我们该如何处理。如果你在编辑器中打开这个 README.md
文件,会看到类似下面的内容:
<<<<<<< HEAD
=======
文件中可能包含这些内容的多个条目。你可能还会看到类似 >>>>>>> swtest/gh-pages
的注释,这些是 Git 不知道如何解决的冲突。你可以手动解决这些冲突。在这种情况下,我只需要 dom-examples
仓库根目录下的 README.md
文件内容,所以我会清理这些冲突或者从 GitHub 上复制 README.md
的内容。
按照 Git 的提示,我们将添加并提交修改。
git add .
git commit -m 'merging sw-test into dom-examples'
以上结果生成了以下输出:
❯ git commit
[146-chore-move-sw-test-into-dom-examples 4300221] Merge remote-tracking branch 'swtest/gh-pages' into 146-chore-move-sw-test-into-dom-examples
如果我现在在项目根目录下执行 git log
,我会看到以下内容:
commit 4300221fe76d324966826b528f4a901c5f17ae20 (HEAD -> 146-chore-move-sw-test-into-dom-examples)
Merge: cdfd2ae 70c0e1e
Author: Schalk Neethling
Date: Sat Aug 13 14:02:48 2022 +0200
Merge remote-tracking branch 'swtest/gh-pages' into 146-chore-move-sw-test-into-dom-examples
commit 70c0e1e53ddb7d7a26e746c4a3412ccef5a683d3 (swtest/gh-pages)
Merge: 4b7cfb2 d4a042d
Author: Schalk Neethling
Date: Sat Aug 13 13:30:58 2022 +0200
Merge pull request #47 from mdn/move-prettierrc
chore: move prettierrc
commit d4a042df51ab65e60498e949ffb2092ac9bccffc (swtest/move-prettierrc)
Author: Schalk Neethling
Date: Sat Aug 13 13:29:56 2022 +0200
chore: move prettierrc
Move `.prettierrc` into the siple-service-worker folder
commit 4b7cfb239a148095b770602d8f6d00c9f8b8cc15
Merge: 8fdfe73 c86d1a1
Author: Schalk Neethling
Date: Sat Aug 13 13:22:31 2022 +0200
Merge pull request #46 from mdn/rename-sw-test
太好了!现在,sw-test
的历史记录已经在我们当前的仓库中了!运行ls -A
后,我看到:
❯ ls -A
.git indexeddb-examples screen-wake-lock-api
.gitignore insert-adjacent screenleft-screentop
CODE_OF_CONDUCT.md matchmedia scrolltooptions
LICENSE media server-sent-events
README.md media-session service-worker
abort-api mediaquerylist streams
auxclick payment-request touchevents
canvas performance-apis web-animations-api
channel-messaging-basic picture-in-picture web-crypto
channel-messaging-multimessage pointer-lock web-share
drag-and-drop pointerevents web-speech-api
fullscreen-api reporting-api web-storage
htmldialogelement-basic resize-event web-workers
indexeddb-api resize-observer webgl-examples
当我运行 ls -A service-worker/
命令时,显示结果如下:
❯ ls -A service-worker/
simple-service-worker
最后,通过运行 ls -A service-worker/simple-service-worker/
可以看到如下内容:
❯ ls -A service-worker/simple-service-worker/
.prettierrc README.md image-list.js style.css
CODE_OF_CONDUCT.md app.js index.html sw.js
LICENSE gallery star-wars-logo.jpg
剩下要做的就是把内容推送到远程仓库。
git push origin 146-chore-mo…dom-examples
注意: 不要使用 squash merge 合并这个 pull request,否则所有的提交 (commits) 将会合并为一个单一的提交 (commit)。相反,你应该使用 merge commit。你可以在 GitHub 文档中阅读关于合并方法的所有详细信息。
当你合并了 pull request 后,继续浏览仓库 (repository) 的提交历史。你会发现提交历史是完整且合并的。现在你可以继续删除或归档旧的仓库。
此时,配置远程仓库对我们的目标仓库不再有意义,因此我们可以安全地移除远程仓库。
git remote rm swtest
结论
完成此任务的步骤如下:
# Clone the repository you want to merge
git clone https://github.com/mdn/sw-test.git
cd sw-test
# Create your feature branch
git switch -C prepare-repo-for-move
# NOTE: With older versions of Git you can run:
# git checkout -b prepare-repo-for-move
# Create directories as needed. You may only need one, not two as
# in the example below.
mkdir service-worker
mkdir service-worker/sw-test
# Enable extendedglob so we can use negation
# The command below is for modern versions of ZSH. See earlier
# in the post for examples for bash and older versions of ZSH
set -o extendedglob
# Move everything except hidden files into your subdirectory,
# also, exclude your target directories
mv ^(sw-test|service-worker) service-worker/sw-test
# Move any of the hidden files or folders you _do_ want
# to move into the subdirectory
mv .prettierrc service-worker
# Add and commit your changes
git add .
git commit -m 'Moved all source files into new subdirectory'
# Push your changes to GitHub
git push origin prepare-repo-for-move
# Head over to the repository on GitHub, open and merge your pull request
# Back in the terminal, switch to your `main` branch
git switch main
# Delete your feature branch
# This is not technically required, but I like to clean up after myself :)
git branch -D prepare-repo-for-move
# Pull the changes you just merged
git pull origin main
# Change to the root directory of your target repository
# If you have not yet cloned your target repository, change
# out of your current directory
cd ..
# Clone your target repository
git clone https://github.com/mdn/dom-examples.git
# Change directory
cd dom-examples
# Create a feature branch for the work
git switch -C 146-chore-move-sw-test-into-dom-examples
# Add your merge target as a remote
git remote add -f ssw https://github.com/mdn/sw-test.git
# Merge the merge target and allow unrelated history
git merge swtest/gh-pages --allow-unrelated-histories
# Add and commit your changes
git add .
git commit -m 'merging sw-test into dom-examples'
# Push your changes to GitHub
git push origin 146-chore-move-sw-test-into-dom-examples
# Open the pull request, have it reviewed by a team member, and merge.
# Do not squash merge this pull request, or else all commits will be
# squashed together as a single commit. Instead, you want to use a merge commit.
# Remove the remote for the merge target
git remote rm swtest
希望你现在已经学会了如何使用 mv 命令来排除子目录,设置和查看 shell 配置。此外,你还应该掌握了怎样通过基本的 Git 命令,将一个 Git 仓库的文件内容合并到新的仓库中,同时保留整个提交历史。